// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.eclipse.che.ide.collections.js;

import com.google.gwt.core.client.JavaScriptObject;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.eclipse.che.ide.collections.Array;
import org.eclipse.che.ide.collections.ArrayIterator;

/**
 * Similar to {@link com.google.gwt.core.client.JsArray}, except {@link
 * com.google.gwt.core.client.JsArray} only allows you to store <T extends JavaScriptObject>.
 *
 * <p>This class is a native JS array that lets you store any Object.
 *
 * @param <T> the type of each entry in the array
 */
public class JsoArray<T> extends JavaScriptObject implements Array<T> {

  /** Concatenate 2 arrays. */
  public static final native <T> JsoArray<T> concat(JsoArray<?> a, JsoArray<?> b) /*-{
        return a.concat(b);
    }-*/;

  /**
   * Constructs a new one.
   *
   * @param <M>
   * @return the array
   */
  public static native <M> JsoArray<M> create() /*-{
        return [];
    }-*/;

  /** Casts a Array to a JsoArray. Unsafe, but on client we know we always have a JsoArray. */
  @SuppressWarnings("unchecked")
  public static <M, T extends M> JsoArray<M> from(Array<T> array) {
    return (JsoArray<M>) array;
  }

  public static <M, T extends M> JsoArray<M> from(List<T> list) {
    JsoArray<M> array = create();
    for (T item : list) {
      array.add(item);
    }

    return array;
  }

  /** Creates a JsoArray from a Java array. */
  public static <M> JsoArray<M> from(M... array) {
    JsoArray<M> result = create();
    for (M s : array) {
      result.add(s);
    }
    return result;
  }

  /**
   * Invokes the native string split on a string and returns a JavaScript array. GWT's version of
   * string.split() emulates Java behavior in JavaScript.
   */
  public static native JsoArray<String> splitString(String str, String regexp) /*-{
        return str.split(regexp);
    }-*/;

  protected JsoArray() {}

  /**
   * Adds a value to the end of an array.
   *
   * @param value
   */
  @Override
  public final native void add(T value) /*-{
        this.push(value);
    }-*/;

  /** Adds all of the elements in the given {@code array} to this array. */
  @Override
  public final void addAll(Array<? extends T> array) {
    for (int i = 0, n = array.size(); i < n; ++i) {
      add(array.get(i));
    }
  }

  @Override
  public final void clear() {
    setLength(0);
  }

  @Override
  public final boolean contains(T value) {
    for (int i = 0, n = size(); i < n; i++) {
      if (equalsOrNull(value, get(i))) {
        return true;
      }
    }
    return false;
  }

  /** Returns a new array with the same contents as this array. */
  @Override
  public final native JsoArray<T> copy() /*-{
        return this.slice(0);
    }-*/;

  /**
   * Standard index accessor.
   *
   * @param index
   * @return T stored at index
   */
  @Override
  public final native T get(int index) /*-{
        return this[index];
    }-*/;

  @Override
  public final native int indexOf(T item) /*-{
        return this.indexOf(item);
    }-*/;

  /** @return whether or not this collection is empty */
  @Override
  public final native boolean isEmpty() /*-{
        return (this.length == 0);
    }-*/;

  /**
   * Uses "" as the separator.
   *
   * @return returns a string using the empty string as the separator.
   */
  public final String join() {
    return join("");
  }

  /**
   * Concatenates the Array into a string using the supplied separator to delimit.
   *
   * @param sep
   * @return The array converted to a String
   */
  @Override
  public final native String join(String sep) /*-{
        return this.join(sep);
    }-*/;

  /**
   * Returns the last element in the array.
   *
   * @return the last element
   */
  @Override
  public final native T peek() /*-{
        return this[this.length - 1];
    }-*/;

  /**
   * Pops the end of the array and returns is.
   *
   * @return a value T from the end of the array
   */
  @Override
  public final native T pop() /*-{
        return this.pop();
    }-*/;

  /**
   * @param index
   * @return The element that was removed.
   */
  @Override
  public final T remove(int index) {
    return splice(index, 1).get(0);
  }

  @Override
  public final boolean remove(T value) {
    for (int i = 0, n = size(); i < n; i++) {
      if ((value != null && value.equals(get(i))) || (value == null && get(i) == null)) {
        remove(i);
        return true;
      }
    }

    return false;
  }

  @Override
  public final native void reverse() /*-{
        this.reverse();
    }-*/;

  /**
   * Sets a given value at a specified index.
   *
   * @param index
   * @param value
   */
  @Override
  public final void set(int index, T value) {
    if (index < 0 || index >= size()) {
      throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
    }
    nativeSet(index, value);
  }

  private native void nativeSet(int index, T value) /*-{
        this[index] = value;
    }-*/;

  /**
   * Reset the length of the array.
   *
   * @param newLength the new length of the array
   */
  public final native void setLength(int newLength) /*-{
        this.length = newLength;
    }-*/;

  public final native T shift() /*-{
        return this.shift();
    }-*/;

  /** Prepends an item onto the array */
  public final native int unshift(T value) /*-{
        return this.unshift(value);
    }-*/;

  /** @return the length of the array */
  @Override
  public final native int size() /*-{
        return this.length;
    }-*/;

  /**
   * @param start
   * @param end
   * @return A sub-array starting at start (inclusive) and endint at the end index (exclusive).
   */
  @Override
  public final native JsoArray<T> slice(int start, int end) /*-{
        return this.slice(start, end);
    }-*/;

  /** Sorts the array using the default browser sorting method. Mutates the array. */
  public final native JsoArray<T> sort() /*-{
        return this.sort();
    }-*/;

  @Override
  public final native void sort(Comparator<? super T> comparator) /*-{
        this.sort(function (a, b) {
            return comparator.@java.util.Comparator::compare(Ljava/lang/Object;Ljava/lang/Object;)(a, b);
        });
    }-*/;

  /**
   * Removes n elements found at the specified index.
   *
   * @param index index at which we are removing
   * @param n the number of elements we will remove starting at the index
   * @return an array of elements that were removed
   */
  @Override
  public final native JsoArray<T> splice(int index, int n) /*-{
        return this.splice(index, n);
    }-*/;

  public final List<T> toList() {
    List<T> list = new ArrayList<>();
    for (T t : asIterable()) {
      list.add(t);
    }
    return list;
  }

  /**
   * Removes n elements found at the specified index. And then inserts the specified item at the
   * index
   *
   * @param index index at which we are inserting/removing
   * @param n the number of elements we will remove starting at the index
   * @param item the item we want to add to the array at the index
   * @return an array of elements that were removed
   */
  @Override
  public final native JsoArray<T> splice(int index, int n, T item) /*-{
        return this.splice(index, n, item);
    }-*/;

  private boolean equalsOrNull(T a, T b) {
    return a == b || (a != null && a.equals(b));
  }

  @Override
  public final Iterable<T> asIterable() {
    return new Iterable<T>() {
      @Override
      public Iterator<T> iterator() {
        return new ArrayIterator<T>(JsoArray.this);
      }
    };
  }
}
